Chapter 13
MFC OLE Clients

by Gene Olafsen

In This Chapter

  IDispatch and Its Place in Automation 486
  Interface Definition for Automation Servers 486
  MFC and Automation 491
  Building a Controller 496

This chapter teaches you a number of ways to implement automation clients—often called controllers. This discussion includes details about the IDispatch interface, as well as the various ways you can put automation servers to work in your application.

IDispatch and Its Place in Automation

Any discussion regarding automation usually begins with a description of the IDispatch interface because it is the most recognizable aspect of automation development and its methods form a framework for presentation of automation topics. This is certainly the case for this chapter. However, back in the days when the only material available on the subject was Inside OLE by Kraig Brockschmidt, it sometimes became confusing whether the discussion centered on material that was implemented by the server or the controller. There is a relatively simple set of rules to remember when tackling IDispatch and its place in automation:

  IDispatch identifies four methods whose implementation is provided by the OLE runtime library.
  Automation servers expose interfaces that derive from IDispatch. This differs from nonautomation servers whose interfaces derive directly from IUnknown.
  A controller calls methods on an automation interface through the interface’s Invoke method. That is to say that an interface pointer to the automation-enabled interface is acquired, except that the actual method invocation is performed indirectly through a helper method.

You can understand IDispatch’s place in an automation server/controller system by knowing that the IDispatch methods are implemented by the OLE runtime and exposed by the server object’s custom interface. Notice, too, that the base IUnknown methods are also available to an automation controller; however, the implementation of such methods falls under the jurisdiction of the server object developer and not the OLE runtime. Automation servers differ from other COM object server configurations in which the server exposes the interface methods directly to the client.

Interface Definition for Automation Servers

Normal or nonautomation COM server interfaces derive directly from the IUnknown interface. Automation servers, however, derive from the IDispatch interface. Automation has been around a while, and, as such, both MFC and ATL support it—this is the good news. The bad news is that automation has been around for a while. The earliest definitions of automation interfaces are grounded in a format known as Object Definition Language (ODL). Just as C was the predecessor to C++, so too does ODL predate IDL. Before Visual Studio’s wizards came along to generate a sizeable portion of an interface’s definition, the files were generated by hand and compiled with a command-line utility named MkTypLib. This program is notorious for outputting error messages that are cryptic at best. It turns out that although Visual Studio’s AppWizard and ClassWizard still generate ODL code, a command-line compatibility switch allows the more common MIDL compiler to process the syntax.

A review of IDL and ODL yields the following differences:

  ATL-based projects define interface definitions in IDL syntax.
  MFC-based projects still define interface definitions using an ODL-compatible syntax.

To illustrate the similarities and differences between the languages, I will define an interface with the following characteristics.

The interface whose name is Slang contains two methods and two properties. The first method, Pass2Short, passes two shorts to the server, whereas the second method, Retrieve2Long, retrieves two longs from the server. The property methods, PropShort and PropLong, put and get a single short and long respectively.

It might seem odd that I have decided to define interfaces with such primitive types, but as you will soon find out, automation is very specific regarding the datatypes it marshals between components. That is not to say that there is no way of passing Boolean and string arguments; it is just that there is a specific manner in which to declare such datatypes.

IDL and ATL

The IDL definition of the interface description appears in Listing 13.1 and is the result of an interface definition using the various helper dialogs that Microsoft provides for ATL object creation.

Listing 13.1 The IDL Definition of the Interface Description


import “oaidl.idl”;
import “ocidl.idl”;
    [
        object,
        uuid(D5F6CF73-615F-11D2-B10B-0000861D2934),
        dual,
        helpstring(“ISlang Interface”),
        pointer_default(unique)
    ]
    interface ISlang : IDispatch
    {
        [id(1), helpstring(“method Pass2Short”)]
         HRESULT Pass2Short(short nS1, short nS2);
        [id(2), helpstring(“method Retrieve2Long”)]
         HRESULT Retrieve2Long(long* nL1, long* nL2);
        [propget, id(3), helpstring(“property PropShort”)]
         HRESULT PropShort([out, retval] long *pVal);
        [propput, id(3), helpstring(“property PropShort”)]
         HRESULT PropShort([in] long newVal);
        [propget, id(4), helpstring(“property PropLong”)]
         HRESULT PropLong([out, retval] long *pVal);
        [propput, id(4), helpstring(“property PropLong”)]
         HRESULT PropLong([in] long newVal);
    };
[
    uuid(D5F6CF66-615F-11D2-B10B-0000861D2934),
    version(1.0),
    helpstring(“IDLandATL 1.0 Type Library”)
]
library IDLANDATLLib
{
    importlib(“stdole32.tlb”);
    importlib(“stdole2.tlb”);
    [
        uuid(D5F6CF74-615F-11D2-B10B-0000861D2934),
        helpstring(“Slang Class”)
    ]
    coclass Slang
    {
        [default] interface ISlang;
    };
};

The first entries of any IDL file that is generated by ATL include import statements for oaidl.idl and ocidl.idl. The import statement is similar in function to the #include statement in C/C++, instructing the compiler to include datatypes that are defined in the imported IDL files. In this case, the interface definition relies upon an IDL definition for the IDispatch interface. This definition appears in the \include\OAIDL.IDL file of your Visual Studio installation. The interface derives from IUnknown, as all interfaces ultimately must.



A quick summary of the remaining IDL elements for this file follows:

  Automation methods are assigned a unique id integer in their attribute definition block.
  Each automation property’s get/put combination is assigned the same id integer in its attribute definition block.
  The propput and propget attributes specify property mutator and accessor functions, respectively.
  The retval attribute indicates that an out parameter is to be used as the return value of the method. This attribute is required to be on the last parameter of a propget method.

ODL and MFC

The ODL syntax, which Visual Studio’s AppWizard and ClassWizard generate, differs slightly from the more common IDL file format. Here is the same interface implementation for an MFC-based automation server:

Listing 13.2 The ODL Definition of the Interface Description


[ uuid(D5F6CF78-615F-11D2-B10B-0000861D2934), version(1.0) ]
library ODLandMFC
{
    importlib(“stdole32.tlb”);
    importlib(“stdole2.tlb”);

    //  Primary dispatch interface for Slang

    [ uuid(D5F6CF79-615F-11D2-B10B-0000861D2934) ]
    dispinterface ISlang
    {
        properties:
       // NOTE - ClassWizard will maintain property information here.
            //    Use extreme caution when editing this section.
            //{{AFX_ODL_PROP(Slang)
            [id(1)] short PropShort;
            [id(2)] long PropLong;
            //}}AFX_ODL_PROP

        methods:
       // NOTE - ClassWizard will maintain method information here.
            //    Use extreme caution when editing this section.
            //{{AFX_ODL_METHOD(Slang)
            [id(3)] void Pass2Short(short nS1, short nS2);
            [id(4)] void Retrieve2Long(long* nL1, long* nL2);
            //}}AFX_ODL_METHOD

    };
    //  Class information for Slang
    [ uuid(D5F6CF77-615F-11D2-B10B-0000861D2934) ]
    coclass Slang
    {
        [default] dispinterface ISlang;
    };
    //{{AFX_APPEND_ODL}}
    //}}AFX_APPEND_ODL}}
};

The ODL representation of the interface should not be a shock to your system. In fact, ODL shares all the syntax elements of IDL. You will notice that an attribute block precedes every definition block, as in IDL. There are a few differences worth noting, though.

Unlike an IDL, which commonly imports additional interface definitions outside the scope of any library or interface, ODL files access external library definitions with the importlib statement. An importlib must appear in the scope of the library that requires such definition. Unlike IDL, which imports the source of other .IDL files, ODL’s importlib accesses compiled type library information.

The next difference you will notice is that dispinterface precedes the ISlang definition. Instead of indicating IDispatch inheritance, as in the IDL example, the definition, which Visual Studio generates, exposes the automation interface without a vtable.

Dispinterfaces (Dispatch Interfaces)

The dispinterface certainly increases the number of languages that can take advantage of COM object servers. However, there are differences with non-IDispatch-derived interfaces that should be noted:

  Methods can only use automation-compliant datatypes when specifying parameters in IDL.
  The IDL keyword dispinterface supports the concept of methods and properties in its definition block.
  Dispinterface defines an interface that does not expose a vtable. ODL-style syntax does, however, provide a mechanism for exposing dual interfaces.

Dual Interfaces

Although dispinterfaces might be ideal for languages that make it difficult, if not impossible, to deal with the pointers of a vtable of an ordinary interface, the overhead involved with accessing automation servers in C++ might be unacceptable. Thankfully, there is quite a simple solution to this problem, and it is called dual interfaces.

Dual interfaces are derived from IDispatch, just like dispinterfaces; however, they also expose their vtable. This is really quite convenient because it allows a single server to provide both implementations in the same physical DLL or EXE package (if an automation can be envisioned as residing in the physical universe) and allows the client to select the method with which it is most comfortable accessing the component. After an automation server is installed on a machine, a Visual Basic or VBScript program can access the server’s methods via the Invoke method of the IDispatch interface, whereas a C++ program can access methods directly through the server’s vtable.

Fortunately, Microsoft strongly recommends that whenever you build a COM server component and expose its interfaces, you do so as a dual interface. In fact, the ATL Object Wizard creates components with dual interfaces by default. For developers using MFC, you can support dual interfaces by changing the dispinterface keyword in your .ODL file to dual.

MFC and Automation

Automation is one of the COM technologies that is supported by the Microsoft Foundation Classes for both client-side and server-side development. Under the MFC implementation, the COM basics remain the same. Automation is based upon the IDispatch interface, and Invoke still plays the part of “calling” methods and properties on servers—so far, so good.

Controller

MFC provides a class that aids in the development of automation controllers. An automation controller must be able to connect to an automation server and call the Invoke method on an interface that derives from IDispatch, with a properly defined array of arguments. The beginning of this chapter contains a listing that shows a primitive OLE-SDK level application, performing the functions just described. The COleDispatchDriver shields the developer somewhat from these details. The good news is that although this section demonstrates low-level client-side automation development, the next section illustrates MFC’s capability to exploit type libraries in a manner similar to ATL.

The steps necessary to produce an automation controller are not much different from building a standard COM client:

1.   Identify the CLSID or ProgID of the automation server with which to connect.
2.   Acquire the DISPIDs of the method(s) to invoke.
3.   Configure an argument list for a method.
4.   Call Invoke with the argument list.



The first place to explore when building an automation controller is the COleDispatchDriver class. This class stands alone in the MFC hierarchy—it has no base class. Here is the class definition for ColeDispatchDriver:

class COleDispatchDriver
{
// Constructors
public:
    COleDispatchDriver();
    COleDispatchDriver(LPDISPATCH lpDispatch,
                       BOOL bAutoRelease = TRUE);
    COleDispatchDriver(const COleDispatchDriver& dispatchSrc);

// Attributes
    LPDISPATCH m_lpDispatch;
    BOOL m_bAutoRelease;

// Operations
    BOOL CreateDispatch(REFCLSID clsid, COleException* pError = NULL);
    BOOL CreateDispatch(LPCTSTR lpszProgID,
                        COleException* pError = NULL);

    void AttachDispatch(LPDISPATCH lpDispatch,
                        BOOL bAutoRelease = TRUE);
    LPDISPATCH DetachDispatch();
        // detach and get ownership of m_lpDispatch
    void ReleaseDispatch();

    // helpers for IDispatch::Invoke
    void AFX_CDECL InvokeHelper(DISPID dwDispID, WORD wFlags,
        VARTYPE vtRet, void* pvRet, const BYTE* pbParamInfo, ...);
    void AFX_CDECL SetProperty(DISPID dwDispID, VARTYPE vtProp, ...);
    void GetProperty(DISPID dwDispID, VARTYPE vtProp, void* pvProp) \
         const;

    // special operators
    operator LPDISPATCH();
    const COleDispatchDriver& operator=(const COleDispatchDriver&
                                        dispatchSrc);

// Implementation
public:
    -COleDispatchDriver();
    void InvokeHelperV(DISPID dwDispID, WORD wFlags, VARTYPE vtRet,
        void* pvRet, const BYTE* pbParamInfo, va_list argList);
};

The function InvokeHelperV is not documented in MFC programmer guides. It differs from InvokeHelper in that it defines the last argument as a variable-argument list (va_list) datatype. This variation on InvokeHelper is used internally by MFC in both control (ActiveX) operation and containment.

Connecting to a Server

Identifying the automation server to which COleDispatchDriver is to connect can occur in three ways. Two of the ways require that you have already acquired an IDispatch pointer, whereas the third retrieves an IDispatch pointer by CLSID or ProgID.

Acquiring an IDispatch Connection

The CreateDispatch function comes in two flavors: one that accepts the CLSID of an object that implements the IDispatch interfaces and one that accepts a ProgID. Both functions can also return error state information in an optional COleException structure. The CreateDispatch function loads the object’s server if it is not already loaded and running, performs a QueryInterface for the interface whose identification has been provided, and then performs the necessary initialization for COleDispatchDriver to access the interface methods through InvokeHelper.

Using either of these functions is simply a matter of instantiating a COleDispatchDriver class and calling the appropriate CreateDispatch function:

COleDispatchDriver dispatcher;
dispatcher.CreateDispatch(clsid);
dispatcher.CreateDispatch(progid);

Connecting to an Existing IDispatch Pointer

If your automation controller program already has a pointer to an IDispatch interface, there are two ways to exploit COleDispatchDriver. The first involves passing the pointer to a COleDispatchDriver declared variable during construction:

LPDISPATCH pValidDispatchInterface(__uuid(DispatchObject));
COleDispatchDriver dispatcher(pValidDispatchInterface);

An instance of COleDispatchDriver is created whose name is dispatcher. It is constructed with a pointer to a valid IDispatch interface pointer pValidDispatchInterface.

The second way that COleDispatchDriver can be used with an existing IDispatch pointer involves connecting to an instance of the class:

COleDispatchDriver dispatcher;
LPDISPATCH pValidDispatchInterface(__uuid(DispatchObject));
dispatcher.AttachDispatch(pValidDispatchInterface);

An instance of COleDispatchDriver is instantiated, this time through the default constructor. At some point later, the dispatcher object is associated with an IDispatch interface pointer: pValidDispatchInterface. The second argument of AttachDispatch is bAutoRelease, and it defaults to a TRUE value. This instructs the dispatcher object to call the Release method on pValidDispatchInterface when it goes out of scope. Two other COleDispatchDriver methods allow a single instance of the class to be used with one or more IDispatch pointers. These functions include DetachDispatch and ReleaseDispatch and are exercised in the following example:

COleDispatchDriver dispatcher;
LPDISPATCH pValidDispatchInterface1(__uuid(Dispatch1Object));
LPDISPATCH pValidDispatchInterface2(__uuid(Dispatch2Object));
dispatcher.AttachDispatch(pValidDispatchInterface1);
dispatcherDetachDispatch();
dispatcher.AttachDispatch(pValidDispatchInterface2);
dispatcherDetachDispatch();
dispatcher.AttachDispatch(pValidDispatchInterface1);
dispatcher.ReleaseDispatch();
dispatcher.AttachDispatch(pValidDispatchInterface2);
dispatcher.ReleaseDispatch();

As a final note, COleDispatchDriver exposes the LPDISPATCH member variable, named m_Dispatch. This enables you to bypass the “helper” functions that deal with IDispatch pointer management and allow classes that derive from COleDispatchDriver to directly manipulate its instance data. The following definition for LPDISPATCH appears in the Oleauto.h file:

typedef IDispatch * LPDISPATCH;

Acquiring DISPIDs

Invoking methods on an IDispatch interface requires knowing the DISPID (dispatch ID) for each method or property to be called. The InvokeHelper function in the COleDispatchDriver class is no different; the first parameter requires a valid DISPID. The IDispatch interface defines a method whose name is GetIDsOfNames and whose purpose is to return a DISPID given a method name.



The GetIDsOfNames is called on a valid IDispatch pointer to obtain the dispatch ID of MyMethod:

LPDISPATCH pValidDispatchInterface(__uuid(DispatchObject));
OLECHAR FAR* szName = “MyMethod”;
HRESULT hResult;
DISPID dispid;
hResult = pValidDispatchInterface->GetIDsOfNames(IID_NULL,
1,
LOCALE_SYSTEM_DEFAULT,
&dispid);

Invoking Methods

The heart of method invocation with COleDispatchDriver is named InvokeHelper. This function is defined with a variable argument list that allows it to be invoked (if you will) with a parameter list that matches the method it is calling. The dispatch ID, dispid, was acquired in the GetIDsOfNames method called previously.

long result = 0L;
pValidDispatchInterface->InvokeHelper(dispid, DISPATCH_METHOD,
VT_I4, (void*)&result, VTS_I4, nData);

Server Review

Developing an automation server in MFC is not difficult, with various wizards performing much of the work. The following steps offer a review of the operations you must take in creating a server:

  1. Select the automation object’s deliver vehicle, EXE or DLL.
  2. Define a dispinterface (an interface that derives from IDispatch).
  3. Add methods and properties as required.
  4. Register the server and automation classes.

Building a Controller

An automation controller is the client side of the OLE automation equation. The controller you will construct in this section is designed to exercise the automation server presented in the preceding chapter.

Using COleDispatchDriver

MFC offers the COleDispatchDriver class to help you develop automation controller applications. The COleDispatchDriver class is somewhat orphaned in the MFC framework, as it does not inherit from any other MFC class, nor does any other MFC class derive from it. The class is rather small, consisting of just three constructor variations and seven class methods. The easiest way to use this class is to have ClassWizard do all the work for you. Using ClassWizard’s Add Class option, you can select a file containing a type library and have a COleDispatchDriver class built for you. This option will be explained in the next section. First, let’s explore this class.

Create a project, MFCDispatchDriver, using AppWizard (see Figure 13.1). Select automation and a single document interface; all the other options are taken as default.


Figure 13.1  Creating the MFCDispatchDriver project.

The next step is to add a class to the project that derives from COleDispatchDriver. At first this would seem rather trivial, and it is not that bad, but the New Class dialog is not as helpful as it could be. The New Class dialog is available under the Insert menu. This dialog creates a header and implementation file for the class you specify and adds them to the current project. The dialog allows you to select a base class from a combo box control. Unfortunately, this combo box does not contain an entry for the COleDispatchDriver class. The steps necessary to derive a class from this base class require you to change your Class Type combo box at the top of the dialog to Generic Class. This causes a change in options in the lower section of the dialog box.

Enter the name of the class you want to create; in this case it can be IDrive (see Figure 13.2). In the lower portion of the dialog, derive your class publicly from COleDispatchDriver. This generates the following header file and constructor and destructor stubs in the implementation file:

#if !defined
(AFX_IDRIVE_H__9ECED411_C6A5_11D2_B17B_0000861D2934__INCLUDED_)
#define AFX_IDRIVE_H__9ECED411_C6A5_11D2_B17B_0000861D2934__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
class IDrive : public COleDispatchDriver
{
public:
    IDrive();
    virtual ~IDrive();

};

#endif //
!defined(AFX_IDRIVE_H__9ECED411_C6A5_11D2_B17B_0000861D2934__INCLUDED_)


Figure 13.2  Creating a dispatch driver class.

Before the dialog is dismissed that indicates the base class definition might not be available and might have to be manually added as an #include file, the warning message box shown in Figure 13.3 might appear. You can safely ignore this warning.


Figure 13.3  Header file warning dialog.

The COleDispatchDriver class contains two member variables, m_bAutoRelease and m_lpDispatch. The first variable determines whether or not Release is called on the IDispatch interface when the ReleaseDispatch method is called, or when the object goes out of scope and is destroyed. The second variable contains the IDispatch interface that you operate on when calling member functions of this class. IDispatch interface attachment might occur using one of the classes’ constructors, calling AttachDispatch or indirectly with a CreateDispatch method call. Before attaching a dispatch interface to this class, you must define implementation methods.

Interface Definition

The Autoserv project developed in the OLE Servers directory is the perfect object to exercise with a home-brew controller. The first step is to decide the interface methods that you want to call and create a header definition and a function body. In this case, three methods will be created. These methods will access the single interface method on the server, and the other two will offer accessor and mutator operations on a single property.

class IDrive : public COleDispatchDriver
{
public:
    IDrive();
    virtual -IDrive();

    // interface method
    void TrackInformation(short nIndex, BSTR * Title);

    // property methods
    short GetAlbumLength();
    void  PutAlbumLength(short nLength);
}

The next step is to put some flesh on the bones of these functions and actually call the automation server. You can easily accomplish this task with a helper function; in fact, the function you need is part of the IDrive base case, InvokeHelper. This function performs a lot of the grunt work for you in calling the IDispatch::Invoke method. InvokeHelper throws COleException and COleDispatchException as a result of any error in processing your request.



InvokeHelper supports a variable argument list, but there are only five arguments that you must supply. Table 13.1 summarizes the InvokeHelper arguments.

Table 13.1 A Summary of InvokeHelper’s Arguments

Parameter Description

DispID Identifies the dispatch ID of the property or method to invoke.
dwFlags Can be any of the following four self-explanatory values: DISPATCH_METHOD DISPATCH_PROPERTYGET DISPATCH_PROPERTYPUT DISPATCH_PROPERTYPUTREF
vtRet Identifies the variant type of the return variable. This argument requires one of the VT_ constants that VARENUM defines.
pvRet Address of a variable to contain the return value. This variable must match the datatype that vtRet defines.
pbParamInfo This parameter requires rather strange formatting. You pass the function a null-terminated string and specify the argument types as a space-delimited list of variant identifiers. These identifiers exist as VTS_ constants. This value may be NULL if there is no argument list.
(variable argument list) The last argument or arguments that are to be passed by Invoke to the automation server. These datatypes must match the VTS_ constants, which you give in pbParamInfo.

Using this function is very straightforward. The following sample definition passes a string and three long values to an automation interface method and returns a long value:

long ISample::VerifyRequest(LPCTSTR strConfig, long Context,
                            long bNewRequest, long Flags)
{
    long result;
    static BYTE parms[] = VTS_BSTR VTS_I4 VTS_I4 VTS_I4;
    InvokeHelper(0x6, DISPATCH_METHOD, VT_I4, (void*)&result,
                 parms, strConfig, Context, bNewRequest, Flags);
    return result;
}

Notice that the params variable contains a space-delimited list of VTS_ types and the function accepts arguments, of these types, in the defined order.

Method Implementation

The method bodies for the Autoserv interface are much simpler. These three methods call the single method, TrackInfo, and the single property, AlbumLength, in the primary Autoserv automation interface.

void IDrive::TrackInformation(short nIndex, BSTR * bstrTitle)
{
    DISPID dispid;
    unsigned short* szName = L“TrackInfo”;
    SCODE scodeReturnError = 0L;
    HRESULT hresult = m_lpDispatch->GetIDsOfNames(IID_NULL,
                  &szName, 1, LOCALE_USER_DEFAULT, &dispid);
    static BYTE params[] = VTS_I2 “\x48”;
                           // \x48 = VTS_BSTR and VT_MFCBYREF;
    InvokeHelper(dispid, DISPATCH_METHOD, VT_ERROR,
           (void*) &scodeReturnError, params, nIndex, bstrTitle);
}

short IDrive::GetAlbumLength()
{
    DISPID dispid;
    unsigned short* szName = L“AlbumLength”;
    short nReturnLength = 0L;
    HRESULT hresult = m_lpDispatch->GetIDsOfNames(IID_NULL,
                  &szName, 1, LOCALE_USER_DEFAULT, &dispid);
    InvokeHelper(dispid, DISPATCH_PROPERTYGET, VT_I2,
                 (void*) &nReturnLength, NULL);
    return nReturnLength;
}

void  IDrive::PutAlbumLength(short nLength)
{
    DISPID dispid;
    unsigned short* szName = L“AlbumLength”;
    HRESULT hresult = m_lpDispatch->GetIDsOfNames(IID_NULL,
                  &szName, 1, LOCALE_USER_DEFAULT, &dispid);
    static BYTE params[] = VTS_I2 ;
    InvokeHelper(dispid, DISPATCH_PROPERTYPUT, VT_EMPTY,
                 (void*) NULL, params, nLength);
}

The only strange part of the code is that I failed to identify how to use the VTS_ constants in a way that could identify a BSTR passed by reference. You can step through the InvokeHelper code and see that there appears to be no way of identifying this relationship except to use bit-masks.

Before making calls on your IDrive interface, you must associate the class instance with a dispatch interface. You can accomplish this by calling CreateDispatch with the ProgID of the automation interface in the constructor.

IDrive::IDrive()
{
    CreateDispatch(_T(“Autoserv.Document”));
}

The final step in constructing the controller application is to instantiate an IDrive object, connect the server’s IDispatch interface, and make calls to the methods and properties. Although it presents a user interface that is lacking, the easiest way to perform these functions is in the OnNewDocument function of the application in the CMFCDispatchDriverDoc class.

BOOL CMFCDispatchDriverDoc::OnNewDocument()
{
    if (!CDocument::OnNewDocument())
        return FALSE;

    IDrive driver;
    _bstr_t b(“Call through InvokeHelper”);
    wchar_t* pBstr = (wchar_t*)b;
    driver.TrackInformation(23, &pBstr);
    driver.PutAlbumLength(45);
    short nOut = driver.GetAlbumLength();

    // Indicate success if we get back what we sent
    // using direct access of the property
    if (nOut == 45)
        AfxMessageBox(“Property access a success!”);

    return TRUE;
}

Exercising the Server

That’s all there is to it. Compile and link the code. Executing this program will result in a dialog box that indicates that you successfully set the server’s property, and read back the same value (see Figure 13.4).


Figure 13.4  COLEDispatchDriver success.

Using #import

Visual C++ offers a preprocessor directive to help you work with automation servers in a manner that makes them almost as easy to work with as in Visual Basic or Visual J++. This directive is the #import statement, and it instructs Visual C++ to use type library information in generating wrapper classes for COM interfaces. This command operates on the type library and generates two header files containing C++ source. These files are output to the directory that your /Fo compiler option specifies. Generally this is either the Debug or Release directory under your project. The name of the type library provides the base name for the header files with extensions of .TLH and .TLI. The first file, with the .TLH extension, is known as the Primary Type Library header file. This file consists of seven sections and contains the second file with the .TLI extension. The .TLI file is known as the implementation file. The magic that makes this directive work is that the compiler reads and produces object code for the header file, and then the preprocessor makes the #import appear as a #include of the primary header file.



As a preprocess directive, the #import directive syntax is similar to that of other directives, such as #include. The command can appear in either of the following formats:

#import <filename> [attributes]

#import “filename” [attributes]

In the first angle-bracket form, the preprocessor is instructed to search directories in the path environment variable, followed by the lib environment variable path list and finally any additional include directories that the /I compiler option specifies. This search ordering is common among other preprocessor directives and is familiar to most using these tools.

The filename argument is the name of any file containing the type library information. Path information may precede the filename. The following file types commonly contain type information:


Extension Description

.TLB or .ODL Type library
.DLL Dynamic link library file
.EXE Executable
.OCX ActiveX or OLE control

Additionally, there are two more categories of files that may contain type information that do not have standard file extensions. A compound document may hold a type library, as well as any other file that the LoadTypeLib function can load.

The #import directive supports a number of attributes. Table 13.2 gives the names and uses of each attribute.

Table 13.2 Attributes for the #import Directive

Attribute Description

exclude Excludes the specified items or type libraries from code that is generated in the header files.
high_method_prefix Specifies a prefix to precede the naming of high-level methods and properties.
high_property_prefixes Allows you to substitute prefixes for the standard Get, Put, and PutRef text that appears before property method names.
implementation_only Suppresses generation of the primary header file.
include(...) Forcibly includes definitions of other type libraries or items whose definition occurs in other system files.
inject_statement Places a line of “text” at the beginning of the namespace definition of the type-library hear file.
named_guids Instructs the compiler to define and initialize old-style GUID variables.
no_auto_exclude Disables automatic exclusion of item definitions.
no_implementation Suppresses generation of the .TLI file.
no_namespace The namespace, whose specification resides in the library statement, is not used.
raw_dispinterfaces Instructs the compiler to generate all method and property calls through Invoke with HRESULT error code return. High-level wrappers are not generated.
raw_interfaces_only Allows you to expose only the low-level contents of the type library, suppressing the generation of higher-level wrapper functions.
raw_method_prefix The compiler substitutes the name you provide here for the raw_ prefix that it normally attaches to low-level member functions.
raw_native_types Forces the use of low-level datatypes, such as BSTR and VARIANT instead of “_bstr_t” and “_variant_t”.
raw_property_prefixes Allows you to specify the low-level prefix for property put, get, and putref methods.
rename Allows you to rename the type library. This is useful in resolving namespace collisions.
rename_namespace Allows you to define the namespace that contains the type library contents.

It is now time to use the #import directive. You will see how easy it is to build an automation controller using this Visual C++ feature, importing the Autoserv project from the last chapter.

Creating a Project

Create a new project with the name controllerimport using the MFC AppWizard for executables, and select an SDI document model. The #import command will appear in the document file, so add the following line to your controllerimportDoc.cpp file:

#import “..\autoserv\Debug\autoserv.tlb”

The type library file may reside in your Release directory if you only built the example with this option. Compiling the project will result in the creation of two additional files in your Debug directory. Again, this may be your Release directory, depending on your project settings. An autoserv.tlh file is your primary type library and autoserv.tli is the implementation file.



TLH File

The primary type library file for the Autoserv type library appears below. This file can consist of the seven sections. Only six sections follow. The seventh section is optional, containing old-style GUID definitions. The option to generate these statements is not necessary.

Header Boilerplate

The first section identifies the source from which the compiler generated this code. In this case, the path to the Autoserv type library is also included. Two pragma directives appear, which specify that this file is only included once by the compiler with the second defining packing alignment for structures. Next is the #include directive for <comdef.h>. This header file contains definitions for classes and templates that are used by this header source code. Specifically, this file contains definitions for _bstr_t, _variant_t, _com_error, and _com_ptr. Finally, a namespace of Autoserv is declared for a region that defines the rest of this file.

// Created by Microsoft (R) C/C++ Compiler Version 12.00.8168.0
//
// f:\mfc unleashed code\controllerimport\debug\autoserv.tlh
//
// C++ source equivalent of Win32 type library
//      ..\autoserv\Debug\autoserv.tlb
// compiler-generated file created 02/15/99 at 20:22:23 - DO NOT EDIT!
#pragma once
#pragma pack(push, 8)

#include <comdef.h>

namespace Autoserv {

Forward References and Typedefs

This second section, as its name implies, contains forward references to structures and typedefs that are used prior to definition later in the file:

struct __declspec(uuid(“723b4da5-b8aa-11d2-8faf-00105a5d8d6c”))
/* dispinterface */ IAutoserv;
struct /* coclass */ Document;
struct __declspec(uuid(“723b4dba-b8aa-11d2-8faf-00105a5d8d6c”))
/* dispinterface */ ISecondInterface;
struct /* coclass */ SecondInterface;

Smart Pointer Declarations

The smart pointer section of this file goes a long way in making automation servers as simple to work with in C++ as they are in languages such as Visual Basic or Visual J++. Smart pointers encapsulate COM interface pointers. Visual C++ supports this both with a template class, _com_ptr_t, and a typedef, _COM_SMARTPTR_TYPEDEF. Smart pointers help solve one of the biggest problems in working with COM-reference counting.

The template class manages all resource allocation and deallocation for you. This template makes the appropriate calls to the IUnknown methods QueryInterface, AddRef, and Release. The “smart” in smart pointers especially comes into play when using the template’s assignment operator and destructor. The template code is smart enough to perform the necessary AddRef and Release calls based on the object assignment, and Release will be called for you when the object goes out of scope. The template eliminates the need for calling AddRef and Release directly.

Smart pointers, however, are usually referenced by the _COM_SMARTPTR_TYPEDEF macro. This macro requires an interface name and the IID of the interface for which you want to acquire a smart pointer. The first argument is simple to provide—this is simply the text for the name of the template specialization with a Ptr appended to the end. Thus, the following code contains IAutoserv as the first macro argument. The resulting parameterized template will have the name IAutoservPtr. The second macro argument requires the IID of an interface. IIDs are obscure structures that contain the unique hexadecimal value for the interface; fortunately, the Visual C++ compiler includes the reserve word __uuid, which retrieves this value for a given type name, reference, variable, or pointer. In the cases that follow, this value is retrieved for the IDispatch interface definition.

A final advantage to using smart pointers that are based upon the _com_ptr_t template is that error conditions are returned as exceptions. The _com_error class encapsulates the HRESULT error code, thus saving you the trouble of having to inspect this value for every call. You can use smart pointers for COM interface manipulation outside of automation and specifically these header files; just remember to include <comdef.h>. In addition, this header file contains smart pointer classes for almost every documented interface. Thus, if you want to use the smart pointer for the IShellIcon interface, you simply declare an object of type IshellIconPtr:

_COM_SMARTPTR_TYPEDEF(IAutoserv, __uuidof(IDispatch));
_COM_SMARTPTR_TYPEDEF(ISecondInterface, __uuidof(IDispatch));

Typeinfo Declarations

This section consists primarily of class definitions. Two definitions appear in this section: IAutoserv and ISecondInterface. The most obscure of the actions that are taken in this section revolves around the definition of properties. The __declspec keyword is a Microsoft extension allowing COM properties to be accessed as member functions. The command accepts a number of attributes; among them is the property statement. This attribute creates “virtual data members” in a class or structure definition.

The virtual data members have the effect of enabling you to manipulate property values directly without the need for calling a member function. When the compiler sees a data member of this type being accessed as a member selection operator (such as “.” or “->”), a corresponding put or get function is substituted. This substitution depends on the side of the expression on which the property exists. That is, an l-value position will result in a put operation and an r-value position results in a get operation. The compiler is even smart enough to handle such complex statements as a -= condition, performing both a get and a put.

//
// Type library items
//

struct __declspec(uuid(“723b4da5-b8aa-11d2-8faf-00105a5d8d6c”))
IAutoserv : IDispatch
{
    //
    // Property data
    //
    __declspec(property(get=GetAlbumLength,put=PutAlbumLength))
    short AlbumLength;
    //
    // Wrapper methods for error-handling
    //

    // Methods:
    SCODE TrackInfo (
        short Index,
        BSTR * Title );

    // Properties:
    short GetAlbumLength ( );
    void PutAlbumLength ( short _val );
};

struct __declspec(uuid(“723b4da3-b8aa-11d2-8faf-00105a5d8d6c”))
Document;
    // [ default ] dispinterface IAutoserv

struct __declspec(uuid(“723b4dba-b8aa-11d2-8faf-00105a5d8d6c”))
ISecondInterface : IDispatch
{
    //
    // Property data
    //

    __declspec(property(get=GetInspect,put=PutInspect))
    _bstr_t Inspect;

    //
    // Wrapper methods for error-handling
    //

    // Methods:
    SCODE SetCounter (
        short Count );

    // Properties:
    _bstr_t GetInspect ( );
    void PutInspect ( _bstr_t _val );
};

struct __declspec(uuid(“723b4dbb-b8aa-11d2-8faf-00105a5d8d6c”))
SecondInterface;
    // [ default ] dispinterface ISecondInterface

//
// Wrapper method implementations
//



Implementation

The implementation section simply includes the .TLI header file and closes the namespace definition block:

#include “f:\mfc unleashed code\controllerimport\debug\autoserv.tli”
} // namespace Autoserv

Footer Boilerplate

The last section returns the structure alignment setting to its previous state:

#pragma pack(pop)

TLI FILE

The .TLI file contains the implementation code for the method declarations that appear in the Typeinfo Declaration section of the .TLH file. This file can be thought of as the equivalent of a .CPP file. For each method, the appropriate Invoke call is made on the IDispatch interface. This invoke operation requires identification of the method or property’s DispID value and an argument list whose datatypes are automation-compatible:

//
// dispinterface IAutoserv wrapper method implementations
//

inline SCODE IAutoserv::TrackInfo ( short Index, BSTR * Title ) {
    SCODE _result;
    _com_dispatch_method(this, 0x2, DISPATCH_METHOD, VT_ERROR,
                         (void*)&_result,
        L“\x0002\x4008”, Index, Title);
    return _result;
}

inline short IAutoserv::GetAlbumLength ( ) {
    short _result;
    _com_dispatch_propget(this, 0x1, VT_I2, (void*)&_result);
    return _result;
}

inline void IAutoserv::PutAlbumLength ( short _val ) {
    _com_dispatch_propput(this, 0x1, VT_I2, _val);
}

//
// dispinterface ISecondInterface wrapper method implementations
//
inline SCODE ISecondInterface::SetCounter ( short Count ) {
    SCODE _result;
    _com_dispatch_method(this, 0x2, DISPATCH_METHOD, VT_ERROR,
                         (void*)&_result,
        L“\x0002”, Count);
    return _result;
}

inline _bstr_t ISecondInterface::GetInspect ( ) {
    BSTR _result;
    _com_dispatch_propget(this, 0x1, VT_BSTR, (void*)&_result);
    return _bstr_t(_result, false);
}

inline void ISecondInterface::PutInspect ( _bstr_t _val ) {
    _com_dispatch_propput(this, 0x1, VT_BSTR, (BSTR)_val);
}

Building this project should not result in any compiler errors or warnings. However, it is not uncommon to have namespace collisions when using the #import directive.

Putting the Server to Work

Now it is time to put your server through its paces. The last thing to do is actually write code that uses the wrapper classes. The code to exercise the server will be put in the OnNewDocument method of the CControllerimportDoc class. In this way the controller will immediately make calls to the server, and you can rerun the code to perform these functions by selecting New from the File menu.

The wrapper classes raise exceptions, whose information is contained in _com_error classes; therefore, the controller code must appear in a try block. The catch block of this exception handler will report the error in a message box.

    try {
        // automation controller code goes here
    }
    catch(const _com_error& e){
        TCHAR buf[255]={0};
        wsprintf(buf,_T(“0x%0”),e.Error());
        ::MessageBox(NULL,buf,_T(“Automation Error”),MB_OK);
    }

The first statement in the try block creates the automation controller. Accomplishing this is as easy as instantiating a class whose definition appears in the header files that the #import statement generates. An object ptr is created for the interface that was defined in the document object of the Autoserv automation server. In creating that server, the default name for the File Type ID was used. This value is Document. You can verify that this is true by looking at the struct definition in your .TLH file.

struct __declspec(uuid(“723b4da3-b8aa-11d2-8faf-00105a5d8d6c”))
Document;

struct __declspec(uuid(“723b4dbb-b8aa-11d2-8faf-00105a5d8d6c”))
SecondInterface;

The constructor acquires the CLSID for the server using the __uuidof keyword and any of the valid expression arguments, including a type name, pointer, reference, template specialization, and so on. In this case you will use the Autoserve::Document definition. Thus, here’s the line of code that returns a smart pointer to this interface on the automation server:

Autoserve::IAutoservPtr ptr(__uuidof(Autoserve::Document));

Exercising the server is now just a matter of making method calls on the ptr object. Visual C++’s smart tooltip identifies the methods and properties that are available to this object as you type (just as it does for every other object you reference). Add the following lines to call a method and set a property:

        // Manipulate the interface through method calls
        _bstr_t b(“test”);
        wchar_t* p2 = (wchar_t*)b;
        ptr->PutAlbumLength(10);
        ptr->TrackInfo(3, &p2);

Now you can prove that your server is actually performing these functions by retrieving the property value that you set.

        short nReturnValue = ptr->GetAlbumLength();

Finally, you can exploit the advanced compiler options that Microsoft provides by directly manipulating properties without calling mutator or accessor methods:

        // Access the properties directly
        ptr->AlbumLength = 5;
        short nReturnLength = ptr->AlbumLength;

When you have finished, your code looks like this:

    try {
        // Create an automation server and access IAutoserv
        Autoserv::IAutoservPtr ptr(__uuidof(Autoserv::Document));

        // Manipulate the interface through method calls
        _bstr_t b(“test”);
        wchar_t* p2 = (wchar_t*)b;
        ptr->PutAlbumLength(10);
        ptr->TrackInfo(3, &p2);
        short nReturnValue = ptr->GetAlbumLength();

        // Indicate success if we get back what we sent
        // using method calls on a property
        if (nReturnValue == 10)
            AfxMessageBox(“Server method access a success!”);

        // Access the properties directly
        ptr->AlbumLength = 5;
        short nReturnLength = ptr->AlbumLength;

        // Indicate success if we get back what we sent
        // using direct access of the property
        if (nReturnLength == 5)
            AfxMessageBox(“Direct property access a success!”);

        // Create an automation server and access ISecondInterface
        Autoserv::ISecondInterfacePtr ptr2(__\
        uuidof(Autoserv::SecondInterface));
        ptr2->SetCounter(15);
    }
    catch(const _com_error& e){
        TCHAR buf[255]={0};
        wsprintf(buf,_T(“0x%0”),e.Error());
        ::MessageBox(NULL,buf,_T(“Automation Error”),MB_OK);
    }



It is now time to put your automation server to work. Compile and link the controllerimport project. When you run the program, you will see dialog boxes appear indicating that you successfully set a property value and read the same value back (see Figure 13.5). Success!


Figure 13.5  Controller success dialog.

Selecting New from the File menu will create a new document and rerun the automation code. What you might be asking yourself at this point is, “Where is the automation server program? I don’t see the window and its statistics information.” Well, you are right, the Autoserv application window never appeared. There is code in that project to present some well-formatted output of the internal server state. This effort shouldn’t go to waste. Let’s look at the code from that project:

    // Check to see if launched as OLE server
    if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated)
    {
        // Register all OLE server (factories) as running.
        // This enables the
        //  OLE libraries to create objects from other applications.
        COleTemplateServer::RegisterAll();

        // Application was run with /Embedding or /Automation.
        // Don’t show the
        //  main window in this case.
        return TRUE;
    }

Looking at the InitInstance code, you will see that AppWizard wires in some code that automatically keeps the application frame hidden when starting under automation control. You can remove this line or comment it out and the server will be visible:

//        return TRUE;

Rebuild the server and rerun the controller application. This time the Autoserv application appears when your controller program executes (see Figure 13.6). The server will also display information in its view as the controller changes the server’s state.


Figure 13.6  The Autoserv automation server’s application frame and view.

Remote Automation

One of the strengths of COM, and more specifically automation, is the concept of location transparency. This term identifies the fact that it isn’t important where an object resides for it to be put to use by another object or client. For many cases, this transparency refers to the fact that the object server need not exist in the process space of the client, that is, the server may be running as a separate program. Such a configuration is referred to as out-of-proc, meaning out of process. The server may also reside in the process space of the caller in the form of a DLL; this is referred to as in-proc, or in-process. The transparency issue comes into play in the fact that the client need not concern itself with the details of whether the server object is in-proc or out-of-proc. The OLE system DLLs manage these details.

NT version 4.0 and Windows 98 extend the notion of location transparency to include activation and use of servers residing on different machines. A set of utilities and DLLs are also available for remote automation on Windows 95 as well. Hence it is now possible for a server, either written by you or not, to reside on a remote machine and be used by a client or controller process on a different machine.

Remote automation allows programs to invoke implementations of IDispatch across a network on different machines. There is complete transparency on the part of both the object server and client, and complete marshaling support is offered for supported automation datatypes. The amazing part of remote automation is the fact that all this happens without the need to change a line of code!

You must follow a number of steps to enable remote automation both on the client application machine and on the server object’s machine.

The first thing you should do is to test the automation server and client on a single machine. Actually, this is a bit more than a suggestion. The purpose of loading and executing the server application on your client machine is to allow the programs to perform the necessary registration.

Registration of the server application can occur by a number of means. If the server is a full server, that is it can run in a standalone fashion, simply executing it will cause the Registry to update. If the server is a DLL, you have two options. You can use the REGSVR32.EXE program, providing it the name and path of the DLL. Otherwise, if your server has a .REG file, you can merge the contents with your Registry. Visual C++ generates .REG file contents for your projects; this file is generally used by your installation program. If you don’t have an installation program, you must edit this file to provide the specific path location for your server—by default this file is created without such information. Finally, if the server is one you built, and it doesn’t perform automatic registration, add the code yourself.



The second step involves using one of the two remote automation utilities. Microsoft provides two utilities, the Remote Automation Connection (RACMGR32.EXE) Manager and the Automation Manager (AUTMGR32.EXE). You will run the first utility, RACMGR32.EXE, on the client machine (see Figure 13.7), that is, the machine running the program that requires the services of a remote automation server.


Figure 13.7  The Remote Automation Connection Manager.

The purpose of running RACMGR32 on the client machine is to instruct the computer where to go to resolve the automation server load request. You want to identify the serving computer from the Server Connection tab. A list box to the left of the dialog contains the COM classes. Locate the server you want to address remotely and select it. In the Network Address combo box, enter the name of the machine that will serve the automation object (see Figure 13.8). (You can see how it was important for you to register the automation server application or DLL on this machine in the first step, or this utility would not be able to add the server object to the list box as it scans the Registry.)


Figure 13.8  Configuration of Autoserv automation server object.

In this case, the Autoserv server object’s network address is being set to \\strattonserver. The Network Protocol combo box contains the following choices:

  none
  TCP/IP
  SPX
  Named Pipes
  NetBIOS over NetBEUI
  NetBIOS over TCP
  NetBIOS over SPX
  Datagrams - IPX
  Datagrams - UDP

The selection is set to TCP/IP. You might have to speak to your network administrator in determining the protocol appropriate for your network. The machines you intend to connect may not support some protocols, or there may be security or routing issues involved in your selection. The last combo box on this tab, Authentication Level, offers the following choices:

  None
  Default
  No Authentication
  Connect
  Call
  Packet
  Packet Integrity
  Packet Privacy

The final client-side step in configuration using this utility is selecting local or remote operation for the server. You accomplish this by switching to the Client Access tab of the utility. You are given four System Security Policy options (see Figure 13.9).

For demonstration purposes, the Allow All Remote Creates option is selected. It is now time to move over to the machine on which the automation server will execute.


Figure 13.9  Client Access options.

On the machine that is to host the automation object, install and register the application or DLL as appropriate. If the application is a standalone program, executing it will generally cause self-registration. Otherwise, the .REG file may have to be merged with the Registry or the REGSVR32.EXE may have to be used. When you are significantly convinced that you have been successful in your registration endeavors, you are ready to configure remote activation.

Run the RACMGR32.EXE program again, this time on the server machine. Select the Client Access tab pane to choose the activation model. There are four choices, just as there were when this utility was executed on the client machine. The default for this option is Remote Creates by Key. This is the option that you will continue with. You must also select the Allow Remote Activation check box at the bottom of the dialog as well.

Selecting the Allow Remote Creates (ACL) option under the Windows NT Operating System requires you to edit the ACL list. The Edit ACL button will be enabled under these conditions, from which you make the appropriate assignment (see Figure 13.10).

Now it is time to use the second automation utility—AUTMGR32.EXE. For remote activation of automation servers, you must have the Automation Manager utility installed and running on the server computer. Generally this program is copied either to the Windows system directory or somewhere on the machine’s path. Executing the AUTMGR32.EXE utility displays a small dialog window. After a few seconds and a couple of initialization messages, the window will automatically minimize itself. If you activate this task, you will see the number of connections and the number of objects in use on your machine (see Figure 13.11).


Figure 13.10  The Remote Class Permissions dialog.


Figure 13.11  The Automation Manager dialog.

It is now time to test remote automation. This is relatively simple: Run the controllerimport program on your client machine. After a brief period of time the success dialog boxes should appear.

Summary

You have seen a number of ways to use MFC to construct automation servers. Between the MFC classes and the Visual Studio wizards, you shouldn’t have any trouble putting automation to use in your applications. A final note of thanks should go to the Microsoft C++ preprocessor, which offers the powerful #import command.